Scenarios¶
Multi-year planning with uncertain demand scenarios.
This notebook introduces:
- Periods: Multiple planning years with different conditions
- Scenarios: Uncertain futures (mild vs. harsh winter)
- Scenario weights: Probability-weighted optimization
- Multi-dimensional data: Parameters that vary by time, period, and scenario
Setup¶
In [1]:
Copied!
import numpy as np
import pandas as pd
import plotly.express as px
import flixopt as fx
fx.CONFIG.notebook()
import numpy as np import pandas as pd import plotly.express as px import flixopt as fx fx.CONFIG.notebook()
Out[1]:
flixopt.config.CONFIG
The Planning Problem¶
We're designing a heating system with:
- 3 periods (years): 2024, 2025, 2026 - gas prices expected to rise
- 2 scenarios: "Mild Winter" (60% probability) and "Harsh Winter" (40% probability)
- Investment decision: Size of CHP unit (made once, works across all futures)
The optimizer finds the investment that minimizes expected cost across all scenarios.
Define Dimensions¶
In [2]:
Copied!
# Time horizon: one representative winter week
timesteps = pd.date_range('2024-01-15', periods=168, freq='h') # 7 days
# Planning periods (years)
periods = pd.Index([2024, 2025, 2026], name='period')
# Scenarios with probabilities
scenarios = pd.Index(['Mild Winter', 'Harsh Winter'], name='scenario')
scenario_weights = np.array([0.6, 0.4]) # 60% mild, 40% harsh
print(f'Time dimension: {len(timesteps)} hours')
print(f'Periods: {list(periods)}')
print(f'Scenarios: {list(scenarios)}')
print(f'Scenario weights: {dict(zip(scenarios, scenario_weights, strict=False))}')
# Time horizon: one representative winter week timesteps = pd.date_range('2024-01-15', periods=168, freq='h') # 7 days # Planning periods (years) periods = pd.Index([2024, 2025, 2026], name='period') # Scenarios with probabilities scenarios = pd.Index(['Mild Winter', 'Harsh Winter'], name='scenario') scenario_weights = np.array([0.6, 0.4]) # 60% mild, 40% harsh print(f'Time dimension: {len(timesteps)} hours') print(f'Periods: {list(periods)}') print(f'Scenarios: {list(scenarios)}') print(f'Scenario weights: {dict(zip(scenarios, scenario_weights, strict=False))}')
Time dimension: 168 hours
Periods: [2024, 2025, 2026]
Scenarios: ['Mild Winter', 'Harsh Winter']
Scenario weights: {'Mild Winter': np.float64(0.6), 'Harsh Winter': np.float64(0.4)}
Create Scenario-Dependent Demand Profiles¶
Heat demand differs significantly between mild and harsh winters:
In [3]:
Copied!
hours = np.arange(168)
hour_of_day = hours % 24
# Base daily pattern (kW): higher in morning/evening
daily_pattern = np.select(
[
(hour_of_day >= 6) & (hour_of_day < 9), # Morning peak
(hour_of_day >= 9) & (hour_of_day < 17), # Daytime
(hour_of_day >= 17) & (hour_of_day < 22), # Evening peak
],
[180, 120, 160],
default=100, # Night
).astype(float)
# Add random variation
np.random.seed(42)
noise = np.random.normal(0, 10, len(timesteps))
# Mild winter: lower demand
mild_demand = daily_pattern * 0.8 + noise
mild_demand = np.clip(mild_demand, 60, 200)
# Harsh winter: higher demand
harsh_demand = daily_pattern * 1.3 + noise * 1.5
harsh_demand = np.clip(harsh_demand, 100, 280)
# Create DataFrame with scenario columns (flixopt uses column names to match scenarios)
heat_demand = pd.DataFrame(
{
'Mild Winter': mild_demand,
'Harsh Winter': harsh_demand,
},
index=timesteps,
)
print(f'Mild winter demand: {mild_demand.min():.0f} - {mild_demand.max():.0f} kW')
print(f'Harsh winter demand: {harsh_demand.min():.0f} - {harsh_demand.max():.0f} kW')
hours = np.arange(168) hour_of_day = hours % 24 # Base daily pattern (kW): higher in morning/evening daily_pattern = np.select( [ (hour_of_day >= 6) & (hour_of_day < 9), # Morning peak (hour_of_day >= 9) & (hour_of_day < 17), # Daytime (hour_of_day >= 17) & (hour_of_day < 22), # Evening peak ], [180, 120, 160], default=100, # Night ).astype(float) # Add random variation np.random.seed(42) noise = np.random.normal(0, 10, len(timesteps)) # Mild winter: lower demand mild_demand = daily_pattern * 0.8 + noise mild_demand = np.clip(mild_demand, 60, 200) # Harsh winter: higher demand harsh_demand = daily_pattern * 1.3 + noise * 1.5 harsh_demand = np.clip(harsh_demand, 100, 280) # Create DataFrame with scenario columns (flixopt uses column names to match scenarios) heat_demand = pd.DataFrame( { 'Mild Winter': mild_demand, 'Harsh Winter': harsh_demand, }, index=timesteps, ) print(f'Mild winter demand: {mild_demand.min():.0f} - {mild_demand.max():.0f} kW') print(f'Harsh winter demand: {harsh_demand.min():.0f} - {harsh_demand.max():.0f} kW')
Mild winter demand: 60 - 163 kW Harsh winter demand: 100 - 262 kW
In [4]:
Copied!
# Visualize demand scenarios with plotly
fig = px.line(
heat_demand.iloc[:48],
title='Heat Demand by Scenario (First 2 Days)',
labels={'index': 'Time', 'value': 'kW', 'variable': 'Scenario'},
)
fig.update_traces(mode='lines')
fig
# Visualize demand scenarios with plotly fig = px.line( heat_demand.iloc[:48], title='Heat Demand by Scenario (First 2 Days)', labels={'index': 'Time', 'value': 'kW', 'variable': 'Scenario'}, ) fig.update_traces(mode='lines') fig
Create Period-Dependent Prices¶
Energy prices change across planning years:
In [5]:
Copied!
# Gas prices by period (€/kWh) - expected to rise
gas_prices = np.array([0.06, 0.08, 0.10]) # 2024, 2025, 2026
# Electricity sell prices by period (€/kWh) - CHP revenue
elec_prices = np.array([0.28, 0.34, 0.43]) # Rising with gas
print('Gas prices by period:')
for period, price in zip(periods, gas_prices, strict=False):
print(f' {period}: {price:.2f} €/kWh')
print('\nElectricity sell prices by period:')
for period, price in zip(periods, elec_prices, strict=False):
print(f' {period}: {price:.2f} €/kWh')
# Gas prices by period (€/kWh) - expected to rise gas_prices = np.array([0.06, 0.08, 0.10]) # 2024, 2025, 2026 # Electricity sell prices by period (€/kWh) - CHP revenue elec_prices = np.array([0.28, 0.34, 0.43]) # Rising with gas print('Gas prices by period:') for period, price in zip(periods, gas_prices, strict=False): print(f' {period}: {price:.2f} €/kWh') print('\nElectricity sell prices by period:') for period, price in zip(periods, elec_prices, strict=False): print(f' {period}: {price:.2f} €/kWh')
Gas prices by period: 2024: 0.06 €/kWh 2025: 0.08 €/kWh 2026: 0.10 €/kWh Electricity sell prices by period: 2024: 0.28 €/kWh 2025: 0.34 €/kWh 2026: 0.43 €/kWh
Build the Flow System¶
Initialize with all dimensions:
In [6]:
Copied!
flow_system = fx.FlowSystem(
timesteps=timesteps,
periods=periods,
scenarios=scenarios,
scenario_weights=scenario_weights,
)
flow_system.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('electricity', '#f1c40f', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
print(flow_system)
flow_system = fx.FlowSystem( timesteps=timesteps, periods=periods, scenarios=scenarios, scenario_weights=scenario_weights, ) flow_system.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('electricity', '#f1c40f', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) print(flow_system)
FlowSystem ========== Timesteps: 168 (Hour) [2024-01-15 to 2024-01-21] Periods: 3 (2024, 2025, 2026) Scenarios: 2 (Mild Winter, Harsh Winter) Status: ⚠
Add Components¶
In [7]:
Copied!
flow_system.add_elements(
# === Buses ===
fx.Bus('Electricity', carrier='electricity'),
fx.Bus('Heat', carrier='heat'),
fx.Bus('Gas', carrier='gas'),
# === Effects ===
fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True),
# === Gas Supply (price varies by period) ===
fx.Source(
'GasGrid',
outputs=[
fx.Flow(
'Gas',
bus='Gas',
size=1000,
effects_per_flow_hour=gas_prices, # Array = varies by period
)
],
),
# === CHP Unit (investment decision) ===
fx.linear_converters.CHP(
'CHP',
electrical_efficiency=0.35,
thermal_efficiency=0.50,
electrical_flow=fx.Flow(
'P_el',
bus='Electricity',
# Investment optimization: find optimal CHP size
size=fx.InvestParameters(
minimum_size=0,
maximum_size=100,
effects_of_investment_per_size={'costs': 50}, # 50 €/kW annualized
),
relative_minimum=0.3,
),
thermal_flow=fx.Flow('Q_th', bus='Heat'),
fuel_flow=fx.Flow('Q_fuel', bus='Gas'),
),
# === Gas Boiler (existing backup) ===
fx.linear_converters.Boiler(
'Boiler',
thermal_efficiency=0.90,
thermal_flow=fx.Flow('Q_th', bus='Heat', size=500),
fuel_flow=fx.Flow('Q_fuel', bus='Gas'),
),
# === Electricity Sales (revenue varies by period) ===
fx.Sink(
'ElecSales',
inputs=[
fx.Flow(
'P_el',
bus='Electricity',
size=100,
effects_per_flow_hour=-elec_prices, # Negative = revenue
)
],
),
# === Heat Demand (varies by scenario) ===
fx.Sink(
'HeatDemand',
inputs=[
fx.Flow(
'Q_th',
bus='Heat',
size=1,
fixed_relative_profile=heat_demand, # DataFrame with scenario columns
)
],
),
)
flow_system.add_elements( # === Buses === fx.Bus('Electricity', carrier='electricity'), fx.Bus('Heat', carrier='heat'), fx.Bus('Gas', carrier='gas'), # === Effects === fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True), # === Gas Supply (price varies by period) === fx.Source( 'GasGrid', outputs=[ fx.Flow( 'Gas', bus='Gas', size=1000, effects_per_flow_hour=gas_prices, # Array = varies by period ) ], ), # === CHP Unit (investment decision) === fx.linear_converters.CHP( 'CHP', electrical_efficiency=0.35, thermal_efficiency=0.50, electrical_flow=fx.Flow( 'P_el', bus='Electricity', # Investment optimization: find optimal CHP size size=fx.InvestParameters( minimum_size=0, maximum_size=100, effects_of_investment_per_size={'costs': 50}, # 50 €/kW annualized ), relative_minimum=0.3, ), thermal_flow=fx.Flow('Q_th', bus='Heat'), fuel_flow=fx.Flow('Q_fuel', bus='Gas'), ), # === Gas Boiler (existing backup) === fx.linear_converters.Boiler( 'Boiler', thermal_efficiency=0.90, thermal_flow=fx.Flow('Q_th', bus='Heat', size=500), fuel_flow=fx.Flow('Q_fuel', bus='Gas'), ), # === Electricity Sales (revenue varies by period) === fx.Sink( 'ElecSales', inputs=[ fx.Flow( 'P_el', bus='Electricity', size=100, effects_per_flow_hour=-elec_prices, # Negative = revenue ) ], ), # === Heat Demand (varies by scenario) === fx.Sink( 'HeatDemand', inputs=[ fx.Flow( 'Q_th', bus='Heat', size=1, fixed_relative_profile=heat_demand, # DataFrame with scenario columns ) ], ), )
Run Optimization¶
In [8]:
Copied!
flow_system.optimize(fx.solvers.HighsSolver(mip_gap=0.01));
flow_system.optimize(fx.solvers.HighsSolver(mip_gap=0.01));
2025-12-17 12:55:41.890 WARNING │ ┌─ Flow CHP(P_el) has a relative_minimum of <xarray.DataArray 'CHP(P_el)|relative_minimum' (time: 168, period: 3, │ │ scenario: 2)> Size: 8kB │ │ array([[[0.3, 0.3], │ │ [0.3, 0.3], │ │ [0.3, 0.3]], │ │ │ │ [[0.3, 0.3], │ │ [0.3, 0.3], │ │ [0.3, 0.3]], │ │ │ │ [[0.3, 0.3], │ │ [0.3, 0.3], │ │ [0.3, 0.3]], │ │ │ │ ..., │ │ │ │ [[0.3, 0.3], │ │ [0.3, 0.3], │ │ [0.3, 0.3]], │ │ │ │ [[0.3, 0.3], │ │ [0.3, 0.3], │ │ [0.3, 0.3]], │ │ │ │ [[0.3, 0.3], │ │ [0.3, 0.3], │ │ [0.3, 0.3]]], shape=(168, 3, 2)) │ │ Coordinates: │ │ * time (time) datetime64[ns] 1kB 2024-01-15 ... 2024-01-21T23:00:00 │ │ * period (period) int64 24B 2024 2025 2026 │ └─ * scenario (scenario) object 16B 'Mild Winter' 'Harsh Winter' and no status_parameters. This prevents the Flow from switching inactive (flow_rate = 0). Consider using status_parameters to allow the Flow to be switched active and inactive.
Writing constraints.: 0%| | 0/30 [00:00<?, ?it/s]
Writing constraints.: 53%|█████▎ | 16/30 [00:00<00:00, 158.12it/s]
Writing constraints.: 100%|██████████| 30/30 [00:00<00:00, 157.02it/s]
Writing continuous variables.: 0%| | 0/28 [00:00<?, ?it/s]
Writing continuous variables.: 100%|██████████| 28/28 [00:00<00:00, 556.56it/s]
Writing binary variables.: 0%| | 0/1 [00:00<?, ?it/s]
Writing binary variables.: 100%|██████████| 1/1 [00:00<00:00, 503.10it/s] Running HiGHS 1.12.0 (git hash: 755a8e0): Copyright (c) 2025 HiGHS under MIT licence terms
MIP linopy-problem-n4itunv5 has 12201 rows; 12198 cols; 36444 nonzeros; 6 integer variables (6 binary)
Coefficient ranges:
Matrix [1e-05, 1e+02]
Cost [4e-01, 6e-01]
Bound [1e+00, 1e+03]
RHS [0e+00, 0e+00]
Presolving model
3036 rows, 2025 cols, 6072 nonzeros 0s
1332 rows, 669 cols, 2664 nonzeros 0s
1326 rows, 666 cols, 2652 nonzeros 0s
Presolve reductions: rows 1326(-10875); columns 666(-11532); nonzeros 2652(-33792)
Solving MIP model with:
1326 rows
666 cols (3 binary, 0 integer, 0 implied int., 663 continuous, 0 domain fixed)
2652 nonzeros
Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero
Nodes | B&B Tree | Objective Bounds | Dynamic Constraints | Work
Src Proc. InQueue | Leaves Expl. | BestBound BestSol Gap | Cuts InLp Confl. | LpIters Time
J 0 0 0 0.00% -inf 5767.622795 Large 0 0 0 0 0.0s
1 0 1 100.00% 5727.336398 5767.622795 0.70% 0 0 0 666 0.0s
Solving report
Model linopy-problem-n4itunv5
Status Optimal
Primal bound 5767.62279485
Dual bound 5727.33639803
Gap 0.698% (tolerance: 1%)
P-D integral 0.00883984914227
Solution status feasible
5767.62279485 (objective)
0 (bound viol.)
0 (int. viol.)
0 (row viol.)
Timing 0.04
Max sub-MIP depth 0
Nodes 1
Repair LPs 0
LP iterations 666
0 (strong br.)
0 (separation)
0 (heuristics)
In [9]:
Copied!
chp_size = flow_system.statistics.sizes['CHP(P_el)']
total_cost = flow_system.solution['costs']
print('=== Investment Decision ===')
print(f'Optimal CHP size: {chp_size.round(1).to_pandas()} kW electrical')
print(f'Thermal capacity: {(chp_size * 0.50 / 0.35).round(1).to_pandas()} kW')
print(f'\nExpected total cost: {total_cost.round(2).to_pandas()} €')
chp_size = flow_system.statistics.sizes['CHP(P_el)'] total_cost = flow_system.solution['costs'] print('=== Investment Decision ===') print(f'Optimal CHP size: {chp_size.round(1).to_pandas()} kW electrical') print(f'Thermal capacity: {(chp_size * 0.50 / 0.35).round(1).to_pandas()} kW') print(f'\nExpected total cost: {total_cost.round(2).to_pandas()} €')
=== Investment Decision === Optimal CHP size: scenario Mild Winter Harsh Winter period 2024 0.0 0.0 2025 0.0 0.0 2026 0.0 0.0 kW electrical Thermal capacity: scenario Mild Winter Harsh Winter period 2024 0.0 0.0 2025 0.0 0.0 2026 0.0 0.0 kW Expected total cost: scenario Mild Winter Harsh Winter period 2024 1153.37 1874.72 2025 1537.82 2499.62 2026 1922.28 3124.53 €
Heat Balance by Scenario¶
See how the system operates differently in each scenario:
In [10]:
Copied!
flow_system.statistics.plot.balance('Heat')
flow_system.statistics.plot.balance('Heat')
Out[10]:
CHP Operation Patterns¶
In [11]:
Copied!
flow_system.statistics.plot.heatmap('CHP(Q_th)')
flow_system.statistics.plot.heatmap('CHP(Q_th)')
Out[11]:
Multi-Dimensional Data Access¶
Results include all dimensions (time, period, scenario):
In [12]:
Copied!
# View dimensions
flow_rates = flow_system.statistics.flow_rates
print('Flow rates dimensions:', dict(flow_rates.sizes))
# Plot flow rates
flow_system.statistics.plot.flows()
# View dimensions flow_rates = flow_system.statistics.flow_rates print('Flow rates dimensions:', dict(flow_rates.sizes)) # Plot flow rates flow_system.statistics.plot.flows()
Flow rates dimensions: {'period': 3, 'scenario': 2, 'time': 169}
Out[12]:
In [13]:
Copied!
# CHP operation in harsh winter vs mild winter
chp_heat = flow_rates['CHP(Q_th)']
print('CHP Heat Output Statistics:')
for scenario in scenarios:
scenario_data = chp_heat.sel(scenario=scenario)
print(f'\n{scenario}:')
for period in periods:
period_data = scenario_data.sel(period=period)
print(f' {period}: avg={period_data.mean().item():.1f} kW, max={period_data.max().item():.1f} kW')
# CHP operation in harsh winter vs mild winter chp_heat = flow_rates['CHP(Q_th)'] print('CHP Heat Output Statistics:') for scenario in scenarios: scenario_data = chp_heat.sel(scenario=scenario) print(f'\n{scenario}:') for period in periods: period_data = scenario_data.sel(period=period) print(f' {period}: avg={period_data.mean().item():.1f} kW, max={period_data.max().item():.1f} kW')
CHP Heat Output Statistics: Mild Winter: 2024: avg=0.0 kW, max=0.0 kW 2025: avg=0.0 kW, max=0.0 kW 2026: avg=0.0 kW, max=0.0 kW Harsh Winter: 2024: avg=0.0 kW, max=0.0 kW 2025: avg=0.0 kW, max=0.0 kW 2026: avg=0.0 kW, max=0.0 kW
Sensitivity: What if Only Mild Winter?¶
Compare optimal CHP size if we only planned for mild winters:
In [14]:
Copied!
# Select only the mild winter scenario
fs_mild = flow_system.transform.sel(scenario='Mild Winter')
fs_mild.optimize(fx.solvers.HighsSolver(mip_gap=0.01))
chp_size_mild = fs_mild.statistics.sizes['CHP(P_el)']
print('=== Comparison ===')
print(f'CHP size (both scenarios): {chp_size.max("scenario").round(2).values} kW')
print(f'CHP size (mild only): {chp_size_mild.round(2).values} kW')
print(f'\nPlanning for uncertainty adds {(chp_size - chp_size_mild).round(2).values} kW capacity')
# Select only the mild winter scenario fs_mild = flow_system.transform.sel(scenario='Mild Winter') fs_mild.optimize(fx.solvers.HighsSolver(mip_gap=0.01)) chp_size_mild = fs_mild.statistics.sizes['CHP(P_el)'] print('=== Comparison ===') print(f'CHP size (both scenarios): {chp_size.max("scenario").round(2).values} kW') print(f'CHP size (mild only): {chp_size_mild.round(2).values} kW') print(f'\nPlanning for uncertainty adds {(chp_size - chp_size_mild).round(2).values} kW capacity')
2025-12-17 12:55:44.657 WARNING │ ┌─ Flow CHP(P_el) has a relative_minimum of <xarray.DataArray 'CHP(P_el)|relative_minimum' (time: 168, period: 3)> Size: 4kB │ │ array([[0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ ... │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3], │ │ [0.3, 0.3, 0.3]]) │ │ Coordinates: │ │ * time (time) datetime64[ns] 1kB 2024-01-15 ... 2024-01-21T23:00:00 │ │ * period (period) int64 24B 2024 2025 2026 │ └─ scenario <U11 44B 'Mild Winter' and no status_parameters. This prevents the Flow from switching inactive (flow_rate = 0). Consider using status_parameters to allow the Flow to be switched active and inactive.
Running HiGHS 1.12.0 (git hash: 755a8e0): Copyright (c) 2025 HiGHS under MIT licence terms
MIP linopy-problem-r02r_tqw has 6099 rows; 6099 cols; 18219 nonzeros; 3 integer variables (3 binary)
Coefficient ranges:
Matrix [1e-05, 1e+02]
Cost [1e+00, 1e+00]
Bound [1e+00, 1e+03]
RHS [0e+00, 0e+00]
Presolving model
1518 rows, 1014 cols, 3036 nonzeros 0s
948 rows, 477 cols, 1896 nonzeros 0s
948 rows, 477 cols, 1896 nonzeros 0s
Presolve reductions: rows 948(-5151); columns 477(-5622); nonzeros 1896(-16323)
Solving MIP model with:
948 rows
477 cols (3 binary, 0 integer, 0 implied int., 474 continuous, 0 domain fixed)
1896 nonzeros
Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero
Nodes | B&B Tree | Objective Bounds | Dynamic Constraints | Work
Src Proc. InQueue | Leaves Expl. | BestBound BestSol Gap | Cuts InLp Confl. | LpIters Time
J 0 0 0 0.00% -inf 4613.46344 Large 0 0 0 0 0.0s
1 0 1 100.00% 4574.054792 4613.46344 0.85% 0 0 0 470 0.0s
Solving report
Model linopy-problem-r02r_tqw
Status Optimal
Primal bound 4613.46344016
Dual bound 4574.05479227
Gap 0.854% (tolerance: 1%)
P-D integral 0.00823189506654
Solution status feasible
4613.46344016 (objective)
0 (bound viol.)
0 (int. viol.)
0 (row viol.)
Timing 0.02
Max sub-MIP depth 0
Nodes 1
Repair LPs 0
LP iterations 470
0 (strong br.)
0 (separation)
0 (heuristics)
=== Comparison ===
CHP size (both scenarios): [0. 0. 0.] kW
CHP size (mild only): [0. 0. 0.] kW
Planning for uncertainty adds [[0. 0.]
[0. 0.]
[0. 0.]] kW capacity
Energy Flow Sankey¶
A Sankey diagram visualizes the total energy flows through the system:
In [15]:
Copied!
flow_system.statistics.plot.sankey.flows()
flow_system.statistics.plot.sankey.flows()
Out[15]:
Key Concepts¶
Multi-Dimensional FlowSystem¶
flow_system = fx.FlowSystem(
timesteps=timesteps, # Time dimension
periods=periods, # Planning periods (years)
scenarios=scenarios, # Uncertain futures
scenario_weights=weights, # Probabilities
)
Dimension-Varying Parameters¶
| Data Shape | Meaning |
|---|---|
| Scalar | Same for all time/period/scenario |
| Array (n_periods,) | Varies by period |
| Array (n_scenarios,) | Varies by scenario |
| DataFrame with columns | Columns match scenario names |
| Full array (time, period, scenario) | Full specification |
Scenario Optimization¶
The optimizer minimizes expected cost: $$\min \sum_s w_s \cdot \text{Cost}_s$$
where $w_s$ is the scenario weight (probability).
Selection Methods¶
# Select specific scenario
fs_mild = flow_system.transform.sel(scenario='Mild Winter')
# Select specific period
fs_2025 = flow_system.transform.sel(period=2025)
# Select time range
fs_day1 = flow_system.transform.sel(time=slice('2024-01-15', '2024-01-16'))
Summary¶
You learned how to:
- Define multiple periods for multi-year planning
- Create scenarios for uncertain futures
- Use scenario weights for probability-weighted optimization
- Pass dimension-varying parameters (arrays and DataFrames)
- Select specific scenarios or periods for analysis
Next Steps¶
- 08a-Aggregation: Speed up large problems with resampling and clustering
- 08b-Rolling Horizon: Decompose large problems into sequential time segments